Add concept of "staged" deployment
authorColin Walters <walters@verbum.org>
Thu, 22 Feb 2018 20:27:59 +0000 (15:27 -0500)
committerAtomic Bot <atomic-devel@projectatomic.io>
Thu, 12 Apr 2018 14:55:12 +0000 (14:55 +0000)
Add API to write a deployment state to `/run/ostree/staged-deployment`,
along with a systemd service which runs at shutdown time.

This is a big change to the ostree model for hosts,
but it closes a longstanding set of bugs; many, many people have
hit the "losing changes in /etc" problem.  It also avoids
the other problem of racing with programs that modify `/etc`
such as LVM backups:
https://bugzilla.redhat.com/show_bug.cgi?id=1365297

We need this in particular to go to a full-on model for
automatically updated host systems where (like a dual-partition model)
everything is fully prepared and the reboot can be taken
asynchronously.

Closes: https://github.com/ostreedev/ostree/issues/545
Closes: #1503
Approved by: jlebon

23 files changed:
Makefile-boot.am
Makefile-ostree.am
apidoc/ostree-sections.txt
src/boot/ostree-finalize-staged.service [new file with mode: 0644]
src/libostree/libostree-devel.sym
src/libostree/ostree-cmdprivate.c
src/libostree/ostree-cmdprivate.h
src/libostree/ostree-deployment-private.h
src/libostree/ostree-deployment.c
src/libostree/ostree-deployment.h
src/libostree/ostree-sysroot-cleanup.c
src/libostree/ostree-sysroot-deploy.c
src/libostree/ostree-sysroot-private.h
src/libostree/ostree-sysroot.c
src/libostree/ostree-sysroot.h
src/ostree/ot-admin-builtin-deploy.c
src/ostree/ot-admin-builtin-finalize-staged.c [new file with mode: 0644]
src/ostree/ot-admin-builtin-status.c
src/ostree/ot-admin-builtins.h
src/ostree/ot-builtin-admin.c
tests/installed/destructive.yml
tests/installed/destructive/staged-deploy.yml [new file with mode: 0644]
tests/installed/tasks/reboot.yml [new file with mode: 0644]

index d3d2f6738651452674055ffb7252538cae5424fa..5b512b6ce7f87c5ebb74caff2876e0ea94f723c9 100644 (file)
@@ -39,7 +39,7 @@ endif
 
 if BUILDOPT_SYSTEMD
 systemdsystemunit_DATA = src/boot/ostree-prepare-root.service \
-       src/boot/ostree-remount.service
+       src/boot/ostree-remount.service src/boot/ostree-finalize-staged.service
 systemdtmpfilesdir = $(prefix)/lib/tmpfiles.d
 dist_systemdtmpfiles_DATA = src/boot/ostree-tmpfiles.conf
 
@@ -65,6 +65,7 @@ EXTRA_DIST += src/boot/dracut/module-setup.sh \
        src/boot/mkinitcpio/ostree \
        src/boot/ostree-prepare-root.service \
        src/boot/ostree-remount.service \
+       src/boot/ostree-finalize-staged.service \
        src/boot/grub2/grub2-15_ostree \
        src/boot/grub2/ostree-grub-generator \
        $(NULL)
index cccbe3006862c7ff66af7ab53e4189776464c3c5..bdd51a72dbd2dff6ac6c0cb5fb6a08c9ec6cb350 100644 (file)
@@ -67,6 +67,7 @@ ostree_SOURCES += \
        src/ostree/ot-admin-builtin-init-fs.c \
        src/ostree/ot-admin-builtin-diff.c \
        src/ostree/ot-admin-builtin-deploy.c \
+       src/ostree/ot-admin-builtin-finalize-staged.c \
        src/ostree/ot-admin-builtin-undeploy.c \
        src/ostree/ot-admin-builtin-instutil.c \
        src/ostree/ot-admin-builtin-cleanup.c \
index 55f2e7a956f22f770d95da00ab836570bd0e2e63..94206e03e35fb3d75212fa87ee0f3ae7061f1bae 100644 (file)
@@ -170,6 +170,7 @@ ostree_deployment_get_origin
 ostree_deployment_get_origin_relpath
 ostree_deployment_get_unlocked
 ostree_deployment_is_pinned
+ostree_deployment_is_staged
 ostree_deployment_set_index
 ostree_deployment_set_bootserial
 ostree_deployment_set_bootconfig
@@ -506,6 +507,7 @@ ostree_sysroot_cleanup
 ostree_sysroot_prepare_cleanup
 ostree_sysroot_repo
 ostree_sysroot_get_repo
+ostree_sysroot_get_staged_deployment
 ostree_sysroot_init_osname
 ostree_sysroot_deployment_set_kargs
 ostree_sysroot_deployment_set_mutable
@@ -514,6 +516,7 @@ ostree_sysroot_deployment_set_pinned
 ostree_sysroot_write_deployments
 ostree_sysroot_write_deployments_with_options
 ostree_sysroot_write_origin_file
+ostree_sysroot_stage_tree
 ostree_sysroot_deploy_tree
 ostree_sysroot_get_merge_deployment
 ostree_sysroot_query_deployments_for
diff --git a/src/boot/ostree-finalize-staged.service b/src/boot/ostree-finalize-staged.service
new file mode 100644 (file)
index 0000000..570138c
--- /dev/null
@@ -0,0 +1,36 @@
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+# For some implementation discussion, see:
+# https://lists.freedesktop.org/archives/systemd-devel/2018-March/040557.html
+[Unit]
+Description=OSTree Finalize Staged Deployment
+ConditionPathExists=/run/ostree-booted
+DefaultDependencies=no
+
+RequiresMountsFor=/sysroot
+After=basic.target
+Before=multi-user.target final.target
+Conflicts=final.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStop=/usr/bin/ostree admin finalize-staged
+
+[Install]
+WantedBy=multi-user.target
index 3377ae126b616abd79f6ca7deadc8cd3ca312f12..07e11cb6ac8bd80c5f5382f9d83b63f47b29843f 100644 (file)
@@ -19,6 +19,9 @@
 
 /* Add new symbols here.  Release commits should copy this section into -released.sym. */
 LIBOSTREE_2018.5 {
+  ostree_sysroot_stage_tree;
+  ostree_sysroot_get_staged_deployment;
+  ostree_deployment_is_staged;
 } LIBOSTREE_2018.3;
 
 /* Stub section for the stable release *after* this development one; don't
index 49d3f5e5b80b516a58a13ab4dd75e9bfaf6b1115..de82521cc398088f125c0a60035f43ced97e9f40 100644 (file)
@@ -26,7 +26,7 @@
 #include "ostree-core-private.h"
 #include "ostree-repo-pull-private.h"
 #include "ostree-repo-static-delta-private.h"
-#include "ostree-sysroot.h"
+#include "ostree-sysroot-private.h"
 #include "ostree-bootloader-grub2.h"
 
 #include "otutil.h"
@@ -52,7 +52,8 @@ ostree_cmd__private__ (void)
     _ostree_repo_static_delta_dump,
     _ostree_repo_static_delta_query_exists,
     _ostree_repo_static_delta_delete,
-    _ostree_repo_verify_bindings
+    _ostree_repo_verify_bindings,
+    _ostree_sysroot_finalize_staged,
   };
 
   return &table;
index 1ac5a1c827a25fb7bcde411b7cee553d930b2670..592157bf3c76351c9210d8c2a282bf635d027297 100644 (file)
@@ -34,6 +34,7 @@ typedef struct {
   gboolean (* ostree_static_delta_query_exists) (OstreeRepo *repo, const char *delta_id, gboolean *out_exists, GCancellable *cancellable, GError **error);
   gboolean (* ostree_static_delta_delete) (OstreeRepo *repo, const char *delta_id, GCancellable *cancellable, GError **error);
   gboolean (* ostree_repo_verify_bindings) (const char *collection_id, const char *ref_name, GVariant *commit, GError **error);
+  gboolean (* ostree_finalize_staged) (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error);
 } OstreeCmdPrivateVTable;
 
 /* Note this not really "public", we just export the symbol, but not the header */
index 114e2f6331307ea92fce6c463bd06aca3d2001d0..ad77317d79399230607b6b8ee09a09e49d4ef38a 100644 (file)
@@ -36,6 +36,7 @@ G_BEGIN_DECLS
  * @bootconfig: Bootloader configuration
  * @origin: How to construct an upgraded version of this tree
  * @unlocked: The unlocked state
+ * @staged: TRUE iff this deployment is staged
  */
 struct _OstreeDeployment
 {
@@ -50,6 +51,7 @@ struct _OstreeDeployment
   OstreeBootconfigParser *bootconfig;
   GKeyFile *origin;
   OstreeDeploymentUnlockedState unlocked;
+  gboolean staged;
 };
 
 void _ostree_deployment_set_bootcsum (OstreeDeployment *self, const char *bootcsum);
index 75a5bd1daed9eddb69b1b371682890c61433894a..820c2632128a2286a84684204bfd2e8cad7d25db 100644 (file)
@@ -339,3 +339,16 @@ ostree_deployment_is_pinned (OstreeDeployment *self)
     return FALSE;
   return g_key_file_get_boolean (self->origin, OSTREE_ORIGIN_TRANSIENT_GROUP, "pinned", NULL);
 }
+
+/**
+ * ostree_deployment_is_staged:
+ * @self: Deployment
+ *
+ * Returns: `TRUE` if deployment should be "finalized" at shutdown time
+ * Since: 2018.3
+ */
+gboolean
+ostree_deployment_is_staged (OstreeDeployment *self)
+{
+  return self->staged;
+}
index 612222a2aba5a5a0314e312bfdfcb907429e1701..756e39d2b4c84edf872c5d62bccc3174401bbb63 100644 (file)
@@ -73,7 +73,8 @@ OstreeBootconfigParser *ostree_deployment_get_bootconfig (OstreeDeployment *self
 _OSTREE_PUBLIC
 GKeyFile *ostree_deployment_get_origin (OstreeDeployment *self);
 
-
+_OSTREE_PUBLIC
+gboolean ostree_deployment_is_staged (OstreeDeployment *self);
 _OSTREE_PUBLIC
 gboolean ostree_deployment_is_pinned (OstreeDeployment *self);
 
index 1d46222b9a572e788107edde682925db19072838..3698767fc00ae434cabbc003a05f342c8f42066c 100644 (file)
@@ -308,6 +308,15 @@ cleanup_old_deployments (OstreeSysroot       *self,
       g_hash_table_replace (active_boot_checksums, bootcsum, bootcsum);
     }
 
+  /* And also the staged deployment, if any */
+  if (self->staged_deployment)
+    {
+      char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, self->staged_deployment);
+      g_hash_table_replace (active_deployment_dirs, deployment_path, deployment_path);
+      char *bootcsum = g_strdup (ostree_deployment_get_bootcsum (self->staged_deployment));
+      g_hash_table_replace (active_boot_checksums, bootcsum, bootcsum);
+    }
+
   /* Find all deployment directories, both active and inactive */
   g_autoptr(GPtrArray) all_deployment_dirs = NULL;
   if (!list_all_deployment_directories (self, &all_deployment_dirs,
index 927809e93b8abad5de8ab63950d7f757330b5130..b593ce38f8768a800de802df2ef86be99dc54ab9 100644 (file)
@@ -684,10 +684,15 @@ selinux_relabel_dir (OstreeSysroot                 *sysroot,
 static gboolean
 selinux_relabel_var_if_needed (OstreeSysroot                 *sysroot,
                                OstreeSePolicy                *sepolicy,
-                               int                            os_deploy_dfd,
+                               OstreeDeployment              *deployment,
                                GCancellable                  *cancellable,
                                GError                       **error)
 {
+  const char *osdeploypath = glnx_strjoina ("ostree/deploy/", ostree_deployment_get_osname (deployment));
+  glnx_autofd int os_deploy_dfd = -1;
+  if (!glnx_opendirat (sysroot->sysroot_fd, osdeploypath, TRUE, &os_deploy_dfd, error))
+    return FALSE;
+
   /* This is a bit of a hack; we should change the code at some
    * point in the distant future to only create (and label) /var
    * when doing a deployment.
@@ -743,12 +748,10 @@ prepare_deployment_etc (OstreeSysroot         *sysroot,
                         OstreeRepo            *repo,
                         OstreeDeployment      *deployment,
                         int                    deployment_dfd,
-                        OstreeSePolicy       **out_sepolicy,
                         GCancellable          *cancellable,
                         GError               **error)
 {
   GLNX_AUTO_PREFIX_ERROR ("Preparing /etc", error);
-  g_autoptr(OstreeSePolicy) sepolicy = NULL;
 
   struct stat stbuf;
   if (!glnx_fstatat_allow_noent (deployment_dfd, "etc", &stbuf, AT_SYMLINK_NOFOLLOW, error))
@@ -781,7 +784,7 @@ prepare_deployment_etc (OstreeSysroot         *sysroot,
       /* Here, we initialize SELinux policy from the /usr/etc inside
        * the root - this is before we've finalized the configuration
        * merge into /etc. */
-      sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error);
+      g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error);
       if (!sepolicy)
         return FALSE;
       if (ostree_sepolicy_get_name (sepolicy) != NULL)
@@ -796,8 +799,6 @@ prepare_deployment_etc (OstreeSysroot         *sysroot,
 
     }
 
-  if (out_sepolicy)
-    *out_sepolicy = g_steal_pointer (&sepolicy);
   return TRUE;
 }
 
@@ -831,7 +832,6 @@ write_origin_file_internal (OstreeSysroot         *sysroot,
                          ostree_deployment_get_csum (deployment),
                          ostree_deployment_get_deployserial (deployment));
 
-
       gsize len;
       g_autofree char *contents = g_key_file_to_data (origin, &len, error);
       if (!contents)
@@ -2324,46 +2324,47 @@ allocate_deployserial (OstreeSysroot           *self,
   return TRUE;
 }
 
-/**
- * ostree_sysroot_deploy_tree:
- * @self: Sysroot
- * @osname: (allow-none): osname to use for merge deployment
- * @revision: Checksum to add
- * @origin: (allow-none): Origin to use for upgrades
- * @provided_merge_deployment: (allow-none): Use this deployment for merge path
- * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment
- * @out_new_deployment: (out): The new deployment path
- * @cancellable: Cancellable
- * @error: Error
- *
- * Check out deployment tree with revision @revision, performing a 3
- * way merge with @provided_merge_deployment for configuration.
+void
+_ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment,
+                                              char            **override_kernel_argv)
+{
+  /* Create an empty boot configuration; we will merge things into
+   * it as we go.
+   */
+  g_autoptr(OstreeBootconfigParser) bootconfig = ostree_bootconfig_parser_new ();
+  ostree_deployment_set_bootconfig (deployment, bootconfig);
+
+  /* After this, install_deployment_kernel() will set the other boot
+   * options and write it out to disk.
+   */
+  if (override_kernel_argv)
+    {
+      g_autoptr(OstreeKernelArgs) kargs = _ostree_kernel_args_new ();
+      _ostree_kernel_args_append_argv (kargs, override_kernel_argv);
+      g_autofree char *new_options = _ostree_kernel_args_to_string (kargs);
+      ostree_bootconfig_parser_set (bootconfig, "options", new_options);
+    }
+}
+
+/* The first part of writing a deployment. This primarily means doing the
+ * hardlink farm checkout, but we also compute some initial state.
  */
-gboolean
-ostree_sysroot_deploy_tree (OstreeSysroot     *self,
-                            const char        *osname,
-                            const char        *revision,
-                            GKeyFile          *origin,
-                            OstreeDeployment  *provided_merge_deployment,
-                            char             **override_kernel_argv,
-                            OstreeDeployment **out_new_deployment,
-                            GCancellable      *cancellable,
-                            GError           **error)
+static gboolean
+sysroot_initialize_deployment (OstreeSysroot     *self,
+                               const char        *osname,
+                               const char        *revision,
+                               GKeyFile          *origin,
+                               char             **override_kernel_argv,
+                               OstreeDeployment **out_new_deployment,
+                               GCancellable      *cancellable,
+                               GError           **error)
 {
   g_return_val_if_fail (osname != NULL || self->booted_deployment != NULL, FALSE);
 
   if (osname == NULL)
     osname = ostree_deployment_get_osname (self->booted_deployment);
 
-  const char *osdeploypath = glnx_strjoina ("ostree/deploy/", osname);
-  glnx_autofd int os_deploy_dfd = -1;
-  if (!glnx_opendirat (self->sysroot_fd, osdeploypath, TRUE, &os_deploy_dfd, error))
-    return FALSE;
-
   OstreeRepo *repo = ostree_sysroot_repo (self);
-  g_autoptr(OstreeDeployment) merge_deployment = NULL;
-  if (provided_merge_deployment != NULL)
-    merge_deployment = g_object_ref (provided_merge_deployment);
 
   gint new_deployserial;
   if (!allocate_deployserial (self, osname, revision, &new_deployserial,
@@ -2388,66 +2389,328 @@ ostree_sysroot_deploy_tree (OstreeSysroot     *self,
     return FALSE;
 
   _ostree_deployment_set_bootcsum (new_deployment, kernel_layout->bootcsum);
+  _ostree_deployment_set_bootconfig_from_kargs (new_deployment, override_kernel_argv);
 
-  /* Initial empty boot configuration. */
-  g_autoptr(OstreeBootconfigParser) bootconfig = ostree_bootconfig_parser_new ();
-  ostree_deployment_set_bootconfig (new_deployment, bootconfig);
+  if (!prepare_deployment_etc (self, repo, new_deployment, deployment_dfd,
+                               cancellable, error))
+    return FALSE;
 
-  /* Handle kernel arguments. After this, install_deployment_kernel() will set
-   * the other boot options and write it out to disk.
-   */
-  if (override_kernel_argv)
-    {
-      /* We have an override set, use it */
-      g_autoptr(OstreeKernelArgs) kargs = _ostree_kernel_args_new ();
-      _ostree_kernel_args_append_argv (kargs, override_kernel_argv);
-      g_autofree char *new_options = _ostree_kernel_args_to_string (kargs);
-      ostree_bootconfig_parser_set (bootconfig, "options", new_options);
-    }
-  else if (provided_merge_deployment)
+  ot_transfer_out_value (out_new_deployment, &new_deployment);
+  return TRUE;
+}
+
+static gboolean
+sysroot_finalize_deployment (OstreeSysroot     *self,
+                             OstreeDeployment  *deployment,
+                             char             **override_kernel_argv,
+                             OstreeDeployment  *merge_deployment,
+                             GCancellable      *cancellable,
+                             GError           **error)
+{
+  g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment);
+  glnx_autofd int deployment_dfd = -1;
+  if (!glnx_opendirat (self->sysroot_fd, deployment_path, TRUE, &deployment_dfd, error))
+    return FALSE;
+
+  /* Only use the merge if we didn't get an override */
+  if (!override_kernel_argv && merge_deployment)
     {
-      /* Use the merge options by default */
-      OstreeBootconfigParser *merge_bootconfig = ostree_deployment_get_bootconfig (provided_merge_deployment);
+      /* Override the bootloader arguments */
+      OstreeBootconfigParser *merge_bootconfig = ostree_deployment_get_bootconfig (merge_deployment);
       if (merge_bootconfig)
         {
           const char *opts = ostree_bootconfig_parser_get (merge_bootconfig, "options");
-          ostree_bootconfig_parser_set (bootconfig, "options", opts);
+          ostree_bootconfig_parser_set (ostree_deployment_get_bootconfig (deployment), "options", opts);
         }
-    }
 
-  g_autoptr(OstreeSePolicy) sepolicy = NULL;
-  if (!prepare_deployment_etc (self, repo, new_deployment, deployment_dfd,
-                               &sepolicy, cancellable, error))
-    return FALSE;
+    }
 
   if (merge_deployment)
     {
-      if (!merge_configuration_from (self, merge_deployment,
-                                     new_deployment, deployment_dfd,
+      /* And do the /etc merge */
+      if (!merge_configuration_from (self, merge_deployment, deployment, deployment_dfd,
                                      cancellable, error))
         return FALSE;
     }
 
-  if (!selinux_relabel_var_if_needed (self, sepolicy, os_deploy_dfd,
-                                      cancellable, error))
+  g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error);
+  if (!sepolicy)
     return FALSE;
 
+  if (!selinux_relabel_var_if_needed (self, sepolicy, deployment, cancellable, error))
+    return FALSE;
+
+  /* Rewrite the origin using the final merged selinux config, just to be
+   * conservative about getting the right labels.
+   */
+  if (!write_origin_file_internal (self, sepolicy, deployment,
+                                   ostree_deployment_get_origin (deployment),
+                                   GLNX_FILE_REPLACE_NODATASYNC,
+                                   cancellable, error))
+    return FALSE;
+
+  /* Seal it */
   if (!(self->debug_flags & OSTREE_SYSROOT_DEBUG_MUTABLE_DEPLOYMENTS))
     {
-      if (!ostree_sysroot_deployment_set_mutable (self, new_deployment, FALSE,
+      if (!ostree_sysroot_deployment_set_mutable (self, deployment, FALSE,
                                                   cancellable, error))
         return FALSE;
     }
 
-  /* Don't fsync here, as we assume that's all done in
-   * ostree_sysroot_write_deployments().
+  return TRUE;
+}
+
+/**
+ * ostree_sysroot_deploy_tree:
+ * @self: Sysroot
+ * @osname: (allow-none): osname to use for merge deployment
+ * @revision: Checksum to add
+ * @origin: (allow-none): Origin to use for upgrades
+ * @provided_merge_deployment: (allow-none): Use this deployment for merge path
+ * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment
+ * @out_new_deployment: (out): The new deployment path
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Check out deployment tree with revision @revision, performing a 3
+ * way merge with @provided_merge_deployment for configuration.
+ *
+ * While this API is not deprecated, you most likely want to use the
+ * ostree_sysroot_stage_tree() API.
+ */
+gboolean
+ostree_sysroot_deploy_tree (OstreeSysroot     *self,
+                            const char        *osname,
+                            const char        *revision,
+                            GKeyFile          *origin,
+                            OstreeDeployment  *provided_merge_deployment,
+                            char             **override_kernel_argv,
+                            OstreeDeployment **out_new_deployment,
+                            GCancellable      *cancellable,
+                            GError           **error)
+{
+  g_autoptr(OstreeDeployment) deployment = NULL;
+  if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv,
+                                      &deployment, cancellable, error))
+    return FALSE;
+
+  if (!sysroot_finalize_deployment (self, deployment, override_kernel_argv,
+                                    provided_merge_deployment,
+                                    cancellable, error))
+    return FALSE;
+
+  *out_new_deployment = g_steal_pointer (&deployment);
+  return TRUE;
+}
+
+/* Serialize information about a deployment to a variant, used by the staging
+ * code.
+ */
+static GVariant *
+serialize_deployment_to_variant (OstreeDeployment *deployment)
+{
+  g_auto(GVariantBuilder) builder = OT_VARIANT_BUILDER_INITIALIZER;
+  g_variant_builder_init (&builder, (GVariantType*)"a{sv}");
+  g_autofree char *name =
+    g_strdup_printf ("%s.%d", ostree_deployment_get_csum (deployment),
+                     ostree_deployment_get_deployserial (deployment));
+  g_variant_builder_add (&builder, "{sv}", "name",
+                         g_variant_new_string (name));
+  g_variant_builder_add (&builder, "{sv}", "osname",
+                         g_variant_new_string (ostree_deployment_get_osname (deployment)));
+  g_variant_builder_add (&builder, "{sv}", "bootcsum",
+                         g_variant_new_string (ostree_deployment_get_bootcsum (deployment)));
+
+  return g_variant_builder_end (&builder);
+}
+
+static gboolean
+require_str_key (GVariantDict *dict,
+                 const char    *name,
+                 const char   **ret,
+                 GError       **error)
+{
+  if (!g_variant_dict_lookup (dict, name, "&s", ret))
+    return glnx_throw (error, "Missing key: %s", name);
+  return TRUE;
+}
+
+/* Reverse of the above; convert a variant to a deployment. Note that the
+ * deployment may not actually be present; this should be verified by
+ * higher level code.
+ */
+OstreeDeployment *
+_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v,
+                                                     GError  **error)
+{
+  g_autoptr(GVariantDict) dict = g_variant_dict_new (v);
+  const char *name = NULL;
+  if (!require_str_key (dict, "name", &name, error))
+    return FALSE;
+  const char *bootcsum = NULL;
+  if (!require_str_key (dict, "bootcsum", &bootcsum, error))
+    return FALSE;
+  const char *osname = NULL;
+  if (!require_str_key (dict, "osname", &osname, error))
+    return FALSE;
+  g_autofree char *checksum = NULL;
+  gint deployserial;
+  if (!_ostree_sysroot_parse_deploy_path_name (name, &checksum, &deployserial, error))
+    return NULL;
+  return ostree_deployment_new (-1, osname, checksum, deployserial,
+                                bootcsum, -1);
+}
+
+
+/**
+ * ostree_sysroot_stage_tree:
+ * @self: Sysroot
+ * @osname: (allow-none): osname to use for merge deployment
+ * @revision: Checksum to add
+ * @origin: (allow-none): Origin to use for upgrades
+ * @merge_deployment: (allow-none): Use this deployment for merge path
+ * @override_kernel_argv: (allow-none) (array zero-terminated=1) (element-type utf8): Use these as kernel arguments; if %NULL, inherit options from provided_merge_deployment
+ * @out_new_deployment: (out): The new deployment path
+ * @cancellable: Cancellable
+ * @error: Error
+ *
+ * Like ostree_sysroot_deploy_tree(), but "finalization" only occurs at OS
+ * shutdown time.
+ */
+gboolean
+ostree_sysroot_stage_tree (OstreeSysroot     *self,
+                           const char        *osname,
+                           const char        *revision,
+                           GKeyFile          *origin,
+                           OstreeDeployment  *merge_deployment,
+                           char             **override_kernel_argv,
+                           OstreeDeployment **out_new_deployment,
+                           GCancellable      *cancellable,
+                           GError           **error)
+{
+  /* This is a bit of a hack.  When adding a new service we have to end up getting
+   * into the presets for downstream distros; see e.g. https://src.fedoraproject.org/rpms/ostree/pull-request/7
+   *
+   * Then again, it's perhaps a bit nicer to only start the service on-demand anyways.
    */
-  if (!write_origin_file_internal (self, sepolicy, new_deployment, NULL,
-                                   GLNX_FILE_REPLACE_NODATASYNC,
-                                   cancellable, error))
+  const char *const systemctl_argv[] = {"systemctl", "start", "ostree-finalize-staged.service", NULL};
+  int estatus;
+  if (!g_spawn_sync (NULL, (char**)systemctl_argv, NULL, G_SPAWN_SEARCH_PATH,
+                     NULL, NULL, NULL, NULL, &estatus, error))
+    return FALSE;
+  if (!g_spawn_check_exit_status (estatus, error))
+    return FALSE;
+
+  g_autoptr(OstreeDeployment) deployment = NULL;
+  if (!sysroot_initialize_deployment (self, osname, revision, origin, override_kernel_argv,
+                                      &deployment, cancellable, error))
+    return FALSE;
+
+  /* Write out the origin file using the sepolicy from the non-merged root for
+   * now (i.e. using /usr/etc policy, not /etc); in practice we don't really
+   * expect people to customize the label for it.
+   */
+  { g_autofree char *deployment_path = ostree_sysroot_get_deployment_dirpath (self, deployment);
+    glnx_autofd int deployment_dfd = -1;
+    if (!glnx_opendirat (self->sysroot_fd, deployment_path, FALSE,
+                         &deployment_dfd, error))
+      return FALSE;
+    g_autoptr(OstreeSePolicy) sepolicy = ostree_sepolicy_new_at (deployment_dfd, cancellable, error);
+    if (!sepolicy)
+      return FALSE;
+    if (!write_origin_file_internal (self, sepolicy, deployment,
+                                     ostree_deployment_get_origin (deployment),
+                                     GLNX_FILE_REPLACE_NODATASYNC,
+                                     cancellable, error))
+      return FALSE;
+  }
+
+  /* After here we defer action until shutdown. The remaining arguments (merge
+   * deployment, kargs) are serialized to a state file in /run.
+   */
+
+  /* "target" is the staged deployment */
+  g_autoptr(GVariantBuilder) builder = g_variant_builder_new ((GVariantType*)"a{sv}");
+  g_variant_builder_add (builder, "{sv}", "target",
+                         serialize_deployment_to_variant (deployment));
+
+  if (merge_deployment)
+    g_variant_builder_add (builder, "{sv}", "merge-deployment",
+                           serialize_deployment_to_variant (merge_deployment));
+
+  if (override_kernel_argv)
+    g_variant_builder_add (builder, "{sv}", "kargs",
+                           g_variant_new_strv ((const char *const*)override_kernel_argv, -1));
+
+  const char *parent = dirname (strdupa (_OSTREE_SYSROOT_RUNSTATE_STAGED));
+  if (!glnx_shutil_mkdir_p_at (AT_FDCWD, parent, 0755, cancellable, error))
+    return FALSE;
+
+  g_autoptr(GVariant) state = g_variant_ref_sink (g_variant_builder_end (builder));
+  if (!glnx_file_replace_contents_at (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED,
+                                      g_variant_get_data (state), g_variant_get_size (state),
+                                      GLNX_FILE_REPLACE_NODATASYNC,
+                                      cancellable, error))
+    return FALSE;
+
+  /* If we have a previous one, clean it up */
+  if (self->staged_deployment)
+    {
+      if (!_ostree_sysroot_rmrf_deployment (self, self->staged_deployment, cancellable, error))
+        return FALSE;
+    }
+
+  if (!_ostree_sysroot_reload_staged (self, error))
+    return FALSE;
+
+  return TRUE;
+}
+
+/* Invoked at shutdown time by ostree-finalize-staged.service */
+gboolean
+_ostree_sysroot_finalize_staged (OstreeSysroot *self,
+                                 GCancellable  *cancellable,
+                                 GError       **error)
+{
+  /* It's totally fine if there's no staged deployment; perhaps down the line
+   * though we could teach the ostree cmdline to tell systemd to activate the
+   * service when a staged deployment is created.
+   */
+  if (!self->staged_deployment)
+    return TRUE;
+
+  g_assert (self->staged_deployment_data);
+
+  g_autoptr(OstreeDeployment) merge_deployment = NULL;
+  g_autoptr(GVariant) merge_deployment_v = NULL;
+  if (g_variant_lookup (self->staged_deployment_data, "merge-deployment", "@a{sv}",
+                        &merge_deployment_v))
+    {
+      merge_deployment =
+        _ostree_sysroot_deserialize_deployment_from_variant (merge_deployment_v, error);
+      if (!merge_deployment)
+        return FALSE;
+    }
+  g_autofree char **kargs = NULL;
+  g_variant_lookup (self->staged_deployment_data, "kargs", "^a&s", &kargs);
+
+  /* Unlink the staged state now; if we're interrupted in the middle,
+   * we don't want e.g. deal with the partially written /etc merge.
+   */
+  if (!glnx_unlinkat (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, 0, error))
+    return FALSE;
+
+  if (!sysroot_finalize_deployment (self, self->staged_deployment, NULL, merge_deployment,
+                                    cancellable, error))
+    return FALSE;
+
+  /* TODO: Proxy across flags too? */
+  OstreeSysrootSimpleWriteDeploymentFlags flags = 0;
+  if (!ostree_sysroot_simple_write_deployment (self, ostree_deployment_get_osname (self->staged_deployment),
+                                               self->staged_deployment, merge_deployment, flags,
+                                               cancellable, error))
     return FALSE;
 
-  ot_transfer_out_value (out_new_deployment, &new_deployment);
   return TRUE;
 }
 
index 01b370e8a26cc96b2fa16105ac50ecd20007de70..a2f3b869fbe57b7a131ce62b5ea93ee2a83c1375 100644 (file)
@@ -61,6 +61,8 @@ struct OstreeSysroot {
   int bootversion;
   int subbootversion;
   OstreeDeployment *booted_deployment;
+  OstreeDeployment *staged_deployment;
+  GVariant *staged_deployment_data;
   struct timespec loaded_ts;
 
   /* Only access through ostree_sysroot_[_get]repo() */
@@ -71,6 +73,7 @@ struct OstreeSysroot {
 
 #define OSTREE_SYSROOT_LOCKFILE "ostree/lock"
 /* We keep some transient state in /run */
+#define _OSTREE_SYSROOT_RUNSTATE_STAGED "/run/ostree/staged-deployment"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR "/run/ostree/deployment-state/"
 #define _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT "unlocked-development"
 
@@ -105,6 +108,22 @@ _ostree_sysroot_list_deployment_dirs_for_os (int                  deploydir_dfd,
                                              GCancellable        *cancellable,
                                              GError             **error);
 
+void
+_ostree_deployment_set_bootconfig_from_kargs (OstreeDeployment *deployment,
+                                              char            **override_kernel_argv);
+
+gboolean
+_ostree_sysroot_reload_staged (OstreeSysroot *self, GError       **error);
+
+gboolean
+_ostree_sysroot_finalize_staged (OstreeSysroot *self,
+                                 GCancellable  *cancellable,
+                                 GError       **error);
+
+OstreeDeployment *
+_ostree_sysroot_deserialize_deployment_from_variant (GVariant *v,
+                                                     GError  **error);
+
 char *
 _ostree_sysroot_get_origin_relpath (GFile         *path,
                                     guint32       *out_device,
@@ -118,6 +137,8 @@ _ostree_sysroot_rmrf_deployment (OstreeSysroot *sysroot,
                                  GCancellable  *cancellable,
                                  GError       **error);
 
+char * _ostree_sysroot_get_runstate_path (OstreeDeployment *deployment, const char *key);
+
 char *_ostree_sysroot_join_lines (GPtrArray  *lines);
 
 gboolean _ostree_sysroot_query_bootloader (OstreeSysroot     *sysroot,
index f77d7703718b8bcc01372b7a41bed52ac476271f..f4a8eadedbf3805255557572877ea8f6652cab4c 100644 (file)
@@ -82,6 +82,8 @@ ostree_sysroot_finalize (GObject *object)
   g_clear_object (&self->repo);
   g_clear_pointer (&self->deployments, g_ptr_array_unref);
   g_clear_object (&self->booted_deployment);
+  g_clear_object (&self->staged_deployment);
+  g_clear_pointer (&self->staged_deployment_data, (GDestroyNotify)g_variant_unref);
 
   glnx_release_lock_file (&self->lock);
 
@@ -584,14 +586,14 @@ parse_bootlink (const char    *bootlink,
   return TRUE;
 }
 
-static char *
-get_unlocked_development_path (OstreeDeployment *deployment)
+char *
+_ostree_sysroot_get_runstate_path (OstreeDeployment *deployment, const char *key)
 {
   return g_strdup_printf ("%s%s.%d/%s",
                           _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_DIR,
                           ostree_deployment_get_csum (deployment),
                           ostree_deployment_get_deployserial (deployment),
-                          _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
+                          key);
 }
 
 static gboolean
@@ -636,9 +638,10 @@ parse_deployment (OstreeSysroot       *self,
     return FALSE;
 
   /* See if this is the booted deployment */
+  const gboolean root_is_ostree_booted =
+    (self->ostree_booted && self->root_is_sysroot);
   const gboolean looking_for_booted_deployment =
-    (self->ostree_booted && self->root_is_sysroot &&
-     !self->booted_deployment);
+    (root_is_ostree_booted && !self->booted_deployment);
   gboolean is_booted_deployment = FALSE;
   if (looking_for_booted_deployment)
     {
@@ -665,7 +668,8 @@ parse_deployment (OstreeSysroot       *self,
     ostree_deployment_set_origin (ret_deployment, origin);
 
   ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_NONE;
-  g_autofree char *unlocked_development_path = get_unlocked_development_path (ret_deployment);
+  g_autofree char *unlocked_development_path =
+    _ostree_sysroot_get_runstate_path (ret_deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
   struct stat stbuf;
   if (lstat (unlocked_development_path, &stbuf) == 0)
     ret_deployment->unlocked = OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT;
@@ -789,6 +793,60 @@ ensure_repo (OstreeSysroot  *self,
   return TRUE;
 }
 
+/* Reload the staged deployment from the file in /run */
+gboolean
+_ostree_sysroot_reload_staged (OstreeSysroot *self,
+                               GError       **error)
+{
+  GLNX_AUTO_PREFIX_ERROR ("Loading staged deployment", error);
+  const gboolean root_is_ostree_booted =
+    self->ostree_booted && self->root_is_sysroot;
+  if (!root_is_ostree_booted)
+    return TRUE; /* Note early return */
+
+  g_assert (self->booted_deployment);
+
+  g_clear_object (&self->staged_deployment);
+  g_clear_pointer (&self->staged_deployment_data, (GDestroyNotify)g_variant_unref);
+
+  /* Read the staged state from disk */
+  glnx_autofd int fd = -1;
+  if (!ot_openat_ignore_enoent (AT_FDCWD, _OSTREE_SYSROOT_RUNSTATE_STAGED, &fd, error))
+    return FALSE;
+  if (fd != -1)
+    {
+      g_autoptr(GBytes) contents = ot_fd_readall_or_mmap (fd, 0, error);
+      if (!contents)
+        return FALSE;
+      g_autoptr(GVariant) staged_deployment_data =
+        g_variant_new_from_bytes ((GVariantType*)"a{sv}", contents, TRUE);
+      g_autoptr(GVariantDict) staged_deployment_dict =
+        g_variant_dict_new (staged_deployment_data);
+
+      /* Parse it */
+      g_autoptr(GVariant) target = NULL;
+      g_autofree char **kargs = NULL;
+      g_variant_dict_lookup (staged_deployment_dict, "target", "@a{sv}", &target);
+      g_variant_dict_lookup (staged_deployment_dict, "kargs", "^a&s", &kargs);
+      if (target)
+        {
+          self->staged_deployment =
+            _ostree_sysroot_deserialize_deployment_from_variant (target, error);
+          if (!self->staged_deployment)
+            return FALSE;
+          _ostree_deployment_set_bootconfig_from_kargs (self->staged_deployment, kargs);
+          self->staged_deployment_data = g_steal_pointer (&staged_deployment_data);
+          /* We set this flag for ostree_deployment_is_staged() because that API
+           * doesn't have access to the sysroot, which currently has the
+           * canonical "staged_deployment" reference.
+           */
+          self->staged_deployment->staged = TRUE;
+        }
+    }
+
+  return TRUE;
+}
+
 gboolean
 ostree_sysroot_load_if_changed (OstreeSysroot  *self,
                                 gboolean       *out_changed,
@@ -857,6 +915,7 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
 
   g_clear_pointer (&self->deployments, g_ptr_array_unref);
   g_clear_object (&self->booted_deployment);
+  g_clear_object (&self->staged_deployment);
   self->bootversion = -1;
   self->subbootversion = -1;
 
@@ -880,17 +939,23 @@ ostree_sysroot_load_if_changed (OstreeSysroot  *self,
         }
     }
 
-  if (self->ostree_booted && self->root_is_sysroot
-      && !self->booted_deployment)
+  const gboolean root_is_ostree_booted =
+    self->ostree_booted && self->root_is_sysroot;
+  if (root_is_ostree_booted && !self->booted_deployment)
     return glnx_throw (error, "Unexpected state: /run/ostree-booted found and in / sysroot but not in a booted deployment");
 
+  /* Ensure the entires are sorted */
   g_ptr_array_sort (deployments, compare_deployments_by_boot_loader_version_reversed);
+  /* And then set their index variables */
   for (guint i = 0; i < deployments->len; i++)
     {
       OstreeDeployment *deployment = deployments->pdata[i];
       ostree_deployment_set_index (deployment, i);
     }
 
+  if (!_ostree_sysroot_reload_staged (self, error))
+    return FALSE;
+
   /* Determine whether we're "physical" or not, the first time we initialize */
   if (!self->loaded)
     {
@@ -949,6 +1014,20 @@ ostree_sysroot_get_booted_deployment (OstreeSysroot       *self)
   return self->booted_deployment;
 }
 
+/**
+ * ostree_sysroot_get_staged_deployment:
+ * @self: Sysroot
+ *
+ * Returns: (transfer none): The currently staged deployment, or %NULL if none
+ */
+OstreeDeployment *
+ostree_sysroot_get_staged_deployment (OstreeSysroot       *self)
+{
+  g_return_val_if_fail (self->loaded, NULL);
+
+  return self->staged_deployment;
+}
+
 /**
  * ostree_sysroot_get_deployments:
  * @self: Sysroot
@@ -1769,7 +1848,8 @@ ostree_sysroot_deployment_unlock (OstreeSysroot     *self,
       break;
     case OSTREE_DEPLOYMENT_UNLOCKED_DEVELOPMENT:
       {
-        g_autofree char *devpath = get_unlocked_development_path (deployment);
+        g_autofree char *devpath =
+          _ostree_sysroot_get_runstate_path (deployment, _OSTREE_SYSROOT_DEPLOYMENT_RUNSTATE_FLAG_DEVELOPMENT);
         g_autofree char *devpath_parent = dirname (g_strdup (devpath));
 
         if (!glnx_shutil_mkdir_p_at (AT_FDCWD, devpath_parent, 0755, cancellable, error))
index e4763d3755d2af8eb922e50fe287ad609a14c12f..47cbb022c29c9566fc26e05377ba4fcdda25ef33 100644 (file)
@@ -74,6 +74,8 @@ _OSTREE_PUBLIC
 GPtrArray *ostree_sysroot_get_deployments (OstreeSysroot  *self);
 _OSTREE_PUBLIC
 OstreeDeployment *ostree_sysroot_get_booted_deployment (OstreeSysroot *self);
+_OSTREE_PUBLIC
+OstreeDeployment *ostree_sysroot_get_staged_deployment (OstreeSysroot *self);
 
 _OSTREE_PUBLIC
 GFile *ostree_sysroot_get_deployment_directory (OstreeSysroot    *self,
@@ -174,6 +176,17 @@ gboolean ostree_sysroot_deploy_tree (OstreeSysroot     *self,
                                      GCancellable      *cancellable,
                                      GError           **error);
 
+_OSTREE_PUBLIC
+gboolean ostree_sysroot_stage_tree (OstreeSysroot     *self,
+                                    const char        *osname,
+                                    const char        *revision,
+                                    GKeyFile          *origin,
+                                    OstreeDeployment  *merge_deployment,
+                                    char             **override_kernel_argv,
+                                    OstreeDeployment **out_new_deployment,
+                                    GCancellable      *cancellable,
+                                    GError           **error);
+
 _OSTREE_PUBLIC
 gboolean ostree_sysroot_deployment_set_mutable (OstreeSysroot     *self,
                                                 OstreeDeployment  *deployment,
index d9905212eed8cf1f7eee636afb79a5bcc4c7ee98..f6c0c16158015cab0f6eec1971a40be7d0fb4701 100644 (file)
@@ -34,6 +34,7 @@
 #include <glib/gi18n.h>
 
 static gboolean opt_retain;
+static gboolean opt_stage;
 static gboolean opt_retain_pending;
 static gboolean opt_retain_rollback;
 static gboolean opt_not_as_default;
@@ -50,6 +51,7 @@ static GOptionEntry options[] = {
   { "origin-file", 0, 0, G_OPTION_ARG_FILENAME, &opt_origin_path, "Specify origin file", "FILENAME" },
   { "no-prune", 0, 0, G_OPTION_ARG_NONE, &opt_no_prune, "Don't prune the repo when done", NULL},
   { "retain", 0, 0, G_OPTION_ARG_NONE, &opt_retain, "Do not delete previous deployments", NULL },
+  { "stage", 0, 0, G_OPTION_ARG_NONE, &opt_stage, "Complete deployment at OS shutdown", NULL },
   { "retain-pending", 0, 0, G_OPTION_ARG_NONE, &opt_retain_pending, "Do not delete pending deployments", NULL },
   { "retain-rollback", 0, 0, G_OPTION_ARG_NONE, &opt_retain_rollback, "Do not delete rollback deployments", NULL },
   { "not-as-default", 0, 0, G_OPTION_ARG_NONE, &opt_not_as_default, "Append rather than prepend new deployment", NULL },
@@ -157,31 +159,45 @@ ot_admin_builtin_deploy (int argc, char **argv, OstreeCommandInvocation *invocat
 
   g_autoptr(OstreeDeployment) new_deployment = NULL;
   g_auto(GStrv) kargs_strv = _ostree_kernel_args_to_strv (kargs);
-  if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin, merge_deployment,
-                                   kargs_strv, &new_deployment, cancellable, error))
-    return FALSE;
-
-  OstreeSysrootSimpleWriteDeploymentFlags flags = OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN;
-  if (opt_retain)
-    flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN;
-  else
+  if (opt_stage)
     {
-      if (opt_retain_pending)
-        flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING;
-      if (opt_retain_rollback)
-        flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK;
+      if (opt_retain_pending || opt_retain_rollback)
+        return glnx_throw (error, "--stage cannot currently be combined with --retain arguments");
+      if (opt_not_as_default)
+        return glnx_throw (error, "--stage cannot currently be combined with --not-as-default");
+      if (!ostree_sysroot_stage_tree (sysroot, opt_osname, revision, origin, merge_deployment,
+                                      kargs_strv, &new_deployment, cancellable, error))
+        return FALSE;
     }
+  else
+    {
+      if (!ostree_sysroot_deploy_tree (sysroot, opt_osname, revision, origin, merge_deployment,
+                                       kargs_strv, &new_deployment, cancellable, error))
+        return FALSE;
 
-  if (opt_not_as_default)
-    flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT;
-
-  if (!ostree_sysroot_simple_write_deployment (sysroot, opt_osname, new_deployment,
-                                               merge_deployment, flags, cancellable, error))
-    return FALSE;
+      OstreeSysrootSimpleWriteDeploymentFlags flags = OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NO_CLEAN;
+      if (opt_retain)
+        flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN;
+      else
+        {
+          if (opt_retain_pending)
+            flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_PENDING;
+          if (opt_retain_rollback)
+            flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_RETAIN_ROLLBACK;
+        }
+
+      if (opt_not_as_default)
+        flags |= OSTREE_SYSROOT_SIMPLE_WRITE_DEPLOYMENT_FLAGS_NOT_DEFAULT;
+
+      if (!ostree_sysroot_simple_write_deployment (sysroot, opt_osname, new_deployment,
+                                                   merge_deployment, flags, cancellable, error))
+        return FALSE;
+    }
 
-  /* And finally, cleanup of any leftover data.
+  /* And finally, cleanup of any leftover data.  In stage mode, we
+   * don't do a full cleanup as we didn't touch the bootloader.
    */
-  if (opt_no_prune)
+  if (opt_no_prune || opt_stage)
     {
       if (!ostree_sysroot_prepare_cleanup (sysroot, cancellable, error))
         return FALSE;
diff --git a/src/ostree/ot-admin-builtin-finalize-staged.c b/src/ostree/ot-admin-builtin-finalize-staged.c
new file mode 100644 (file)
index 0000000..6740f82
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include "ot-main.h"
+#include "ot-admin-builtins.h"
+#include "ot-admin-functions.h"
+#include "ostree.h"
+#include "otutil.h"
+
+#include "ostree-cmdprivate.h"
+#include "ostree.h"
+
+/* Called by ostree-finalize-staged.service, and in turn
+ * invokes a cmdprivate function inside the shared library.
+ */
+gboolean
+ot_admin_builtin_finalize_staged (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
+{
+  /* Just a sanity check; we shouldn't be called outside of the service though.
+   */
+  struct stat stbuf;
+  if (fstatat (AT_FDCWD, "/run/ostree-booted", &stbuf, 0) < 0)
+    return TRUE;
+
+  g_autoptr(GFile) sysroot_file = g_file_new_for_path ("/");
+  g_autoptr(OstreeSysroot) sysroot = ostree_sysroot_new (sysroot_file);
+
+  if (!ostree_sysroot_load (sysroot, cancellable, error))
+    return FALSE;
+  if (!ostree_cmd__private__()->ostree_finalize_staged (sysroot, cancellable, error))
+    return FALSE;
+
+  return TRUE;
+}
index 096155c688ce2da7c0e260847351d3eff081c8f5..55be69942c87e7e020eac7e14c998cce877483af 100644 (file)
@@ -96,7 +96,9 @@ deployment_print_status (OstreeSysroot    *sysroot,
   GKeyFile *origin = ostree_deployment_get_origin (deployment);
 
   const char *deployment_status = "";
-  if (is_pending)
+  if (ostree_deployment_is_staged (deployment))
+    deployment_status = " (staged)";
+  else if (is_pending)
     deployment_status = " (pending)";
   else if (is_rollback)
     deployment_status = " (rollback)";
@@ -199,6 +201,16 @@ ot_admin_builtin_status (int argc, char **argv, OstreeCommandInvocation *invocat
     }
   else
     {
+      OstreeDeployment *staged = ostree_sysroot_get_staged_deployment (sysroot);
+      if (staged)
+        {
+          if (!deployment_print_status (sysroot, repo, staged,
+                                        FALSE, FALSE, FALSE,
+                                        cancellable,
+                                        error))
+            return FALSE;
+        }
+
       for (guint i = 0; i < deployments->len; i++)
         {
           OstreeDeployment *deployment = deployments->pdata[i];
index a81f4d62d41680973f4993ed0640efa9840d73e7..d88fc0b9077dc14881b71f83905a6f06cdcb8dcc 100644 (file)
@@ -40,6 +40,7 @@ BUILTINPROTO(undeploy);
 BUILTINPROTO(deploy);
 BUILTINPROTO(cleanup);
 BUILTINPROTO(pin);
+BUILTINPROTO(finalize_staged);
 BUILTINPROTO(unlock);
 BUILTINPROTO(status);
 BUILTINPROTO(set_origin);
index 1262c5a589a2affb68e11716b3a659730f0480bf..b26eea8157c66f91bcd751c882e0604d737c6975 100644 (file)
@@ -57,6 +57,9 @@ static OstreeCommand admin_subcommands[] = {
   { "pin", OSTREE_BUILTIN_FLAG_NO_REPO,
     ot_admin_builtin_pin,
     "Change the \"pinning\" state of a deployment" },
+  { "finalize-staged", OSTREE_BUILTIN_FLAG_NO_REPO | OSTREE_BUILTIN_FLAG_HIDDEN,
+    ot_admin_builtin_finalize_staged,
+    "Internal command to run at shutdown time" },
   { "status", OSTREE_BUILTIN_FLAG_NO_REPO,
     ot_admin_builtin_status,
     "List deployments" },
index 9529c7e980d4183a0e29f87409e2f5269f209b29..5bd4d7a7ab9ddedd11a51ad1a34d7b2a6e6d4a54 100644 (file)
     # Next copy all of the tests/ directory
     - name: Copy test data
       synchronize: src=../../ dest=/root/tests/ archive=yes
+
+    # First, the Ansible-based tests
+    - import_tasks: destructive/staged-deploy.yml
+
     - find:
         paths: /root/tests/installed/destructive
         patterns: "itest-*.sh"
diff --git a/tests/installed/destructive/staged-deploy.yml b/tests/installed/destructive/staged-deploy.yml
new file mode 100644 (file)
index 0000000..bf50467
--- /dev/null
@@ -0,0 +1,24 @@
+# Test the deploy --stage functionality
+
+- name: Write staged-deploy commit
+  shell: |
+    ostree --repo=/ostree/repo commit --parent="${commit}" -b staged-deploy --tree=ref="${commit}" --no-bindings
+    ostree admin deploy --stage --karg-proc-cmdline --karg=ostreetest=yes staged-deploy
+  environment:
+    commit: "{{ rpmostree_status['deployments'][0]['checksum'] }}"
+- include_tasks: ../tasks/reboot.yml
+- name: Check that deploy-staged service worked
+  shell: |
+    # Assert that the previous boot had a journal entry for it
+    journalctl -b "-1" -u ostree-finalize-staged.service | grep -q -e 'Transaction complete'
+    # And that we have the new kernel argument
+    grep -q -e 'ostreetest=yes' /proc/cmdline
+- name: Rollback
+  shell: rpm-ostree rollback
+- include_tasks: ../tasks/reboot.yml
+- shell: |
+    ostree refs --delete staged-deploy
+    rpm-ostree cleanup -rp
+# And now we shouldn't have the kernel commandline entry
+- name: Check we do not have new kernel cmdline entry
+  shell: grep -qv -e 'ostreetest=yes' /proc/cmdline
diff --git a/tests/installed/tasks/reboot.yml b/tests/installed/tasks/reboot.yml
new file mode 100644 (file)
index 0000000..fd07710
--- /dev/null
@@ -0,0 +1,71 @@
+# This file is copied from atomic-host-tests
+
+# vim: set ft=ansible:
+# There is no clean way to restart hosts in ansible. The general issue is that
+# the shutdown command may close sshd before ansible has time to "return" from
+# the task, even with async & poll. This is due to the fact that asynchronous
+# tasks still require a small synchronous bootstrapping script which takes 1 sec
+# to complete, during which it is vulnerable to erroring out if sshd dies.
+#       To mitigate this, we prefix a sleep command before the shutdown so
+# ansible has time to move on. For more info on this issue, see:
+# https://github.com/ansible/ansible/issues/10616
+#
+# The Ansible docs now recommend this combination of tasks to handle reboots
+# https://support.ansible.com/hc/en-us/articles/201958037-Reboot-a-server-and-wait-for-it-to-come-back
+
+# remember the real ansible_host for following local actions
+# (otherwise ansible will target the localhost)
+- set_fact:
+    real_ansible_host: "{{ ansible_host }}"
+    timeout: "{{ cli_reboot_timeout | default('120') }}"
+
+# Have to account for both because Fedora STR uses the old version of these
+# inventory values for some reason.
+- when: ansible_port is defined
+  set_fact:
+    real_ansible_port: "{{ ansible_port }}"
+
+- when: ansible_ssh_port is defined
+  set_fact:
+    real_ansible_port: "{{ ansible_ssh_port }}"
+
+- name: Get original bootid
+  command: cat /proc/sys/kernel/random/boot_id
+  register: orig_bootid
+
+- name: restart hosts
+  when: (not skip_shutdown is defined) or (not skip_shutdown)
+  shell: sleep 3 && shutdown -r now
+  async: 1
+  poll: 0
+  ignore_errors: true
+
+# NB: The following tasks use local actions, so we need to explicitly ensure
+# that they don't use sudo, which may require a password, and is not necessary
+# anyway.
+
+- name: wait for hosts to come back up
+  local_action:
+    wait_for host={{ real_ansible_host }}
+    port={{ real_ansible_port | default('22') }}
+    state=started
+    delay=30
+    timeout={{ timeout }}
+    search_regex="OpenSSH"
+  become: false
+
+# I'm not sure the retries are even necessary, but I'm keeping them in
+- name: Wait until bootid changes
+  command: cat /proc/sys/kernel/random/boot_id
+  register: new_bootid
+  until: new_bootid.stdout != orig_bootid.stdout
+  retries: 6
+  delay: 10
+
+# provide an empty iterator when a list is not provided
+# http://docs.ansible.com/ansible/playbooks_conditionals.html#loops-and-conditionals
+- name: check services have started
+  service:
+    name: "{{ item }}"
+    state: started
+  with_items: "{{ wait_for_services|default([]) }}"